/**
 * Copyright (c) 2021-2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef ECMASCRIPT_ECMA_RUNTIM_CALL_INFO_H
#define ECMASCRIPT_ECMA_RUNTIM_CALL_INFO_H

#include <algorithm>

#include "plugins/ecmascript/runtime/common.h"
#include "plugins/ecmascript/runtime/js_tagged_value.h"
#include "plugins/ecmascript/runtime/js_thread.h"
#include "js_handle.h"

namespace ark::ecmascript {
class EcmaRuntimeCallInfo;
using EcmaEntrypoint = JSTaggedValue (*)(EcmaRuntimeCallInfo *);

class EcmaRuntimeCallInfo {
public:
    EcmaRuntimeCallInfo(JSThread *thread, uint32_t numArgs, JSTaggedValue *args)
        : thread_(thread), numArgs_(numArgs), args_(args)
    {
        ASSERT(numArgs >= js_method_args::NUM_MANDATORY_ARGS);
    }

    ~EcmaRuntimeCallInfo() = default;

    inline JSThread *GetThread() const
    {
        return thread_;
    }

    inline void SetNewTarget(JSTaggedValue tagged)
    {
        SetArg(js_method_args::NEW_TARGET_IDX, tagged);
    }

    inline void SetFunction(JSTaggedValue tagged)
    {
        SetArg(js_method_args::FUNC_IDX, tagged);
    }

    inline void SetThis(JSTaggedValue tagged)
    {
        SetArg(js_method_args::THIS_IDX, tagged);
    }

    inline void SetCallArg(uint32_t idx, JSTaggedValue tagged)
    {
        ASSERT_PRINT(idx < GetArgsNumber(), "Can not set values out of index range");
        SetArg(idx + js_method_args::FIRST_ARG_IDX, tagged);
    }

    inline JSHandle<JSTaggedValue> GetFunction() const
    {
        return GetArg(js_method_args::FUNC_IDX);
    }

    inline JSHandle<JSTaggedValue> GetNewTarget() const
    {
        return GetArg(js_method_args::NEW_TARGET_IDX);
    }

    inline JSHandle<JSTaggedValue> GetThis() const
    {
        return GetArg(js_method_args::THIS_IDX);
    }

    inline JSHandle<JSTaggedValue> GetCallArg(uint32_t idx) const
    {
        ASSERT_PRINT(idx < GetArgsNumber(), "Can not set values out of index range");
        return GetArg(idx + js_method_args::FIRST_ARG_IDX);
    }

    /*
     * The number of arguments pairs excluding the 'func', 'new.target' and 'this'. For instance:
     * for code fragment: " foo(v1); ", GetArgsNumber() returns 1
     */
    inline uint32_t GetArgsNumber() const
    {
        return numArgs_ - js_method_args::NUM_MANDATORY_ARGS;
    }

    inline uintptr_t GetArgAddress(uint32_t idx) const
    {
        ASSERT(idx < numArgs_);
        // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
        return reinterpret_cast<uintptr_t>(&args_[idx]);
    }

    template <typename... Args>
    ALWAYS_INLINE inline void SetCallArgs(Args... args);

    inline void SetCallArg(uint32_t argc, const JSTaggedType *argv, uint32_t bias = 0)
    {
        for (uint32_t i = 0; i < argc; i++) {
            // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
            SetCallArg(bias + i, JSTaggedValue(argv[i]));  // NOLINT(clang-analyzer-core.NullDereference)
        }
    }

    inline uint32_t GetInternalArgsNum() const
    {
        return numArgs_;
    }

    inline JSTaggedValue *GetInternalArgs() const
    {
        return args_;
    }

    template <typename T>
    static inline JSTaggedValue UnpackIfHandle(JSHandle<T> v)
    {
        return v.GetTaggedValue();
    }
    static inline JSTaggedValue UnpackIfHandle(JSTaggedValue v)
    {
        return v;
    }

    DEFAULT_COPY_SEMANTIC(EcmaRuntimeCallInfo);
    DEFAULT_MOVE_SEMANTIC(EcmaRuntimeCallInfo);

private:
    EcmaRuntimeCallInfo() = default;
    friend class ScopedCallInfo;

    inline void SetArg(uint32_t idx, JSTaggedValue tagged)
    {
        // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
        args_[idx] = tagged;
    }

    inline JSHandle<JSTaggedValue> GetArg(uint32_t idx) const
    {
        return JSHandle<JSTaggedValue>(GetArgAddress(idx));
    }

private:
    alignas(sizeof(JSTaggedType)) JSThread *thread_ {nullptr};
    alignas(sizeof(JSTaggedType)) uint32_t numArgs_ {0};
    alignas(sizeof(JSTaggedType)) JSTaggedValue *args_ {nullptr};
};

template <typename... Args>
ALWAYS_INLINE inline void EcmaRuntimeCallInfo::SetCallArgs(Args... args)
{
    std::array<JSTaggedType, sizeof...(Args)> argsArr {UnpackIfHandle(args).GetRawData()...};
    SetCallArg(argsArr.size(), argsArr.data(), 0);
}

class ScopedCallInfo {
public:
    ScopedCallInfo() = default;

    ScopedCallInfo(JSThread *thread, uint32_t numArgs)
    {
        numArgs += js_method_args::NUM_MANDATORY_ARGS;
        auto allocator = thread->GetStackFrameAllocator();
        auto *mem = reinterpret_cast<JSTaggedValue *>(
            allocator->Alloc(AlignUp(numArgs * sizeof(JSTaggedValue), GetAlignmentInBytes(DEFAULT_FRAME_ALIGNMENT))));
        LOG_IF(mem == nullptr, FATAL, ECMASCRIPT) << "Cannot allocate ScopedCallInfo frame";
        new (&cinfo_) EcmaRuntimeCallInfo(thread, numArgs, mem);
        AddToChain();
    }

    ~ScopedCallInfo()
    {
        if (cinfo_.thread_ != nullptr) {
            RemoveFromChain();
            auto thread = cinfo_.thread_;
            auto allocator = thread->GetStackFrameAllocator();
            allocator->Free(cinfo_.args_);
        }
    }

    ScopedCallInfo &operator=(ScopedCallInfo &&other)
    {
        auto thread = other.cinfo_.thread_;
        ASSERT(this->cinfo_.thread_ == nullptr);
        ASSERT(thread->scopedCallInfo_ == &other);
        thread->scopedCallInfo_ = this;

        std::swap(this->prev_, other.prev_);
        std::swap(this->cinfo_, other.cinfo_);
        return *this;
    }

    ScopedCallInfo(ScopedCallInfo &&other)
    {
        *this = std::move(other);
    }

    static void IterateChain(JSThread *thread, const RootRangeVisitor &v0)
    {
        for (auto head = thread->scopedCallInfo_; head != nullptr; head = head->prev_) {
            auto *args = head->cinfo_.args_;
            auto numArgs = head->cinfo_.numArgs_;
            // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
            v0(Root::ROOT_HANDLE, ObjectSlot(ToUintPtr(args)), ObjectSlot(ToUintPtr(args + numArgs)));
        }
    }

    inline EcmaRuntimeCallInfo *operator->()
    {
        return &cinfo_;
    }

    inline EcmaRuntimeCallInfo *Get()
    {
        return &cinfo_;
    }

private:
    NO_COPY_SEMANTIC(ScopedCallInfo);

    void AddToChain()
    {
        auto thread = cinfo_.thread_;
        prev_ = thread->scopedCallInfo_;
        thread->scopedCallInfo_ = this;
    }

    void RemoveFromChain()
    {
        auto thread = cinfo_.thread_;
        ASSERT(thread->scopedCallInfo_ == this);
        thread->scopedCallInfo_ = prev_;
    }

    EcmaRuntimeCallInfo cinfo_ {};
    ScopedCallInfo *prev_ {};
};

template <typename TF, typename TT, typename TN>
static inline ScopedCallInfo NewRuntimeCallInfo(JSThread *thread, TF func, TT thisObj, TN newTarget, uint32_t numArgs)
{
    ScopedCallInfo info(thread, numArgs);
    info->SetFunction(EcmaRuntimeCallInfo::UnpackIfHandle(func));
    info->SetThis(EcmaRuntimeCallInfo::UnpackIfHandle(thisObj));
    info->SetNewTarget(EcmaRuntimeCallInfo::UnpackIfHandle(newTarget));
    return info;
}

}  // namespace ark::ecmascript

#endif  // ECMASCRIPT_ECMA_RUNTIM_CALL_INFO_H
